/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package genomemap.maps;

import genomemap.data.Clone;
import genomemap.data.DNA_RegionUtil;
import genomemap.data.DataSource;
import genomemap.data.PDataSource;
import genomemap.data.Probe;

import javautil.collections.ArrayUtil;
import javautil.graphics.GraphicsUtil;
import javautil.lang.MathUtil;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

import javax.swing.JPanel;

/**
 *
 * @author stewari1
 */
public class RepDNADistMap extends JPanel {

    /** Field description */
    private final int MAP_HEADER_VERT_INDENT = 40;

    /** Field description */
    private final int MAP_HEADER_HORI_INDENT = 40;

    /** Field description */
    private final int MAP_HEADER_SCALE_GAP = 10;

    /** Field description */
    private final int MAP_SCALE_HORI_INDENT = 10;


    // keep it multiple of 2

    /** Field description */
    private final int SCALE_TEXT_LINE_GAP = 12;


    // make it a multiple of 12 * 5 * int( each 200 kb gap)

    /** Field description */
    private final int MAX_CH_PIX_SIZE = 600;

    /** Field description */
    private final int INTER_CHROMOSOME_GAP = 50;

    /** Field description */
    private final int CONTIG_HEIGHT = 15;

    /** Field description */
    private final int MAP_LEGEND_HORI_GAP = -45;

    /** Field description */
    private final int MAP_LEGEND_VERT_GAP = 100;

    /** Field description */
    private final int LEGEND_WIDTH = 15;

    /** Field description */
    private final int LEGEND_HEIGHT = 15;

    /** Field description */
    private final int LEGEND_FIG_TEXT_GAP = 15;

    /** Field description */
    private final int INTER_LEGEND_GAP = 20;

    /** Field description */
    private final int LEGEND_HORI_PAD = 10;

    /** Field description */
    private final int LEGEND_VERT_PAD = 10;

    /** Field description */
    private BasicStroke lineStroke = null;

    /** Field description */
    private final Font textFont = new Font("Courier New", Font.BOLD, 12);

    /** Field description */
    private String mapHeader = null;

    /** Field description */
    private int mapWIDTH = 0;

    /** Field description */
    private int mapHEIGHT = 0;

    /** Field description */
    private final Contig[][] contigsArray = new Contig[7][];

    /** Field description */
    private final int[][] contigGapsArray = new int[7][];

    /** Field description */
    private int probeSize = 34;

    /** Field description */
    private int[][] probeOrders = null;

    /** Field description */
    private final int MAX_CH_SIZE_MB = 12;

    /**
     * Constructs ...
     */
    public RepDNADistMap() {
        lineStroke = new BasicStroke(2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    }

    public void instantiate(String mapHeader, int probeSize, int[][] probeOrders,
                            int[][] allProbeSpacings) {

        this.probeSize   = probeSize;
        this.probeOrders = probeOrders;

        for (int chIndex = 0; chIndex < 7; chIndex++) {

            int[] probeOrder    = probeOrders[chIndex];
            int[] probeSpacings = allProbeSpacings[chIndex];
            boolean contigOpen  = true;
            PDataSource pData   = DataSource.getPDataSource(chIndex + 1);

            pData.setOrder(probeOrder);

            int[][] binaryProfile = ArrayUtil.transpose(pData.getHybridData());


            // get the clones and probes : make sure they have their natural order
            Clone[] clones = (Clone[]) new TreeSet(pData.getCloneSet()).toArray(new Clone[0]);
            Probe[] probes = (Probe[]) new TreeSet(pData.getFullProbeSet()).toArray(new Probe[0]);
            List<Contig> contigList        = new ArrayList<>();
            List<Integer> contigCloneSizes = new ArrayList<>();
            List<Integer> contigGapsList   = new ArrayList<>();
            List<Clone> contigCloneList    = new ArrayList<>();

            for (int rowIndex = 0; rowIndex < probeOrder.length - 1; rowIndex++) {

                contigOpen = true;

                contigCloneList.add(probes[probeOrder[rowIndex] - 1]);
                contigCloneSizes.add(new Integer(probeSize));

                if (!DNA_RegionUtil.isConnected(rowIndex, probeOrder, binaryProfile)) {

                    contigOpen = false;

                    contigGapsList.add(new Integer(probeSpacings[rowIndex + 1]));
                    contigList.add(new Contig(contigCloneList, contigCloneSizes));


                    // reset:
                    contigCloneList  = new ArrayList<>();
                    contigCloneSizes = new ArrayList<>();

                } else {

                    contigCloneList.add(getLinkingClone(rowIndex, probeOrder, binaryProfile,
                            clones));
                    contigCloneSizes.add(new Integer(probeSpacings[rowIndex + 1]));
                }
            }


            // !tricky here:  understand the logic below before you change.
            if (!contigOpen) {

                contigCloneList.add(probes[probeOrder[probeOrder.length - 1] - 1]);
                contigCloneSizes.add(new Integer(probeSize));
            }

            contigList.add(new Contig(contigCloneList, contigCloneSizes));

            contigsArray[chIndex] = contigList.toArray(new Contig[0]);
            contigGapsArray[chIndex] =
                ArrayUtil.getIntegerArray(contigGapsList.toArray(new Integer[0]));
        }

        this.mapHeader = mapHeader;
        this.mapHEIGHT = 2 * MAP_HEADER_VERT_INDENT + this.getFontMetrics(textFont).getHeight()
                         + MAP_HEADER_SCALE_GAP + 7 * (INTER_CHROMOSOME_GAP + CONTIG_HEIGHT);
        this.mapWIDTH = 2 * MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP + MAX_CH_PIX_SIZE
                        + MAP_LEGEND_HORI_GAP + LEGEND_FIG_TEXT_GAP + LEGEND_WIDTH
                        + 2 * LEGEND_HORI_PAD
                        + 2 * GraphicsUtil.findWordPixelWidth("VII", this.getFontMetrics(textFont))
        ;

        setPreferredSize(new Dimension(mapWIDTH, mapHEIGHT));
        setBackground(Color.white);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);

        if (lineStroke == null)
            lineStroke = new BasicStroke(2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);

        Graphics2D g2 = (Graphics2D) g;

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setStroke(lineStroke);


        // create background
        g2.setColor(Color.white);
        g2.fillRect(0, 0, mapWIDTH, mapHEIGHT);


        // set up text color and font and rendering hints
        g2.setColor(Color.black);
        g2.setFont(textFont);


        // map header
        g2.drawString(mapHeader, MAP_HEADER_HORI_INDENT, MAP_HEADER_VERT_INDENT);


        // scales : horizontal (12 Mb divide in Mbs and each Mb divided in quarters)
        int horiScaleTextY = MAP_HEADER_VERT_INDENT + this.getFontMetrics(textFont).getHeight()
                             + MAP_HEADER_SCALE_GAP;

        for (int mbIndex = 0; mbIndex <= MAX_CH_SIZE_MB; mbIndex++) {

            String label = "" + mbIndex;
            int x = (int) (MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP
                           + ((double) MAX_CH_PIX_SIZE / MAX_CH_SIZE_MB) * mbIndex);

            g2.drawString(label, x, horiScaleTextY);


            // bigger line - scale
            g2.drawLine(x, horiScaleTextY + SCALE_TEXT_LINE_GAP / 6, x,
                        horiScaleTextY + SCALE_TEXT_LINE_GAP);


            // smaller line - scale
            if (mbIndex < MAX_CH_SIZE_MB) {

                for (int subIndex = 1; subIndex < 5; subIndex++) {

                    int subX = x + (MAX_CH_PIX_SIZE / 60) * subIndex;

                    g2.drawLine(subX, horiScaleTextY + SCALE_TEXT_LINE_GAP / 2, subX,
                                horiScaleTextY + SCALE_TEXT_LINE_GAP);
                }
            }
        }

        g2.drawLine(MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP,
                    horiScaleTextY + SCALE_TEXT_LINE_GAP,
                    MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP + MAX_CH_PIX_SIZE,
                    horiScaleTextY + SCALE_TEXT_LINE_GAP);


        // scale: vertical
        for (int chIndex = 1; chIndex <= 7; chIndex++) {

            String label = "" + chIndex;
            int y        = horiScaleTextY + SCALE_TEXT_LINE_GAP + INTER_CHROMOSOME_GAP * chIndex;
            int x = MAP_SCALE_HORI_INDENT
                    - GraphicsUtil.findWordPixelWidth(label, this.getFontMetrics(textFont));

            g2.drawString(label, x, y);
        }

        g2.drawLine(MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP,
                    horiScaleTextY + SCALE_TEXT_LINE_GAP,
                    MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP,
                    horiScaleTextY + SCALE_TEXT_LINE_GAP + 8 * INTER_CHROMOSOME_GAP);


        // draw contigs
        int x = 0;
        int y = 0;
        int contigSize;
        int contigPixWidth   = 0;
        int estSize          = 0;
        int totalContigCount = 0;    // this variable is used to get some statistics.
        int totalCosmidCloneCount = 0;    // used for stat only: cosmid count is probes with connecting clones. that is all the clones in general in a contig
        int totalContigSize  = 0;    // used for stat only:

        for (int chIndex = 0; chIndex < 7; chIndex++) {

            estSize = 0;
            x       = MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP;

            Contig[] contigArray = contigsArray[chIndex];

            totalContigCount += contigArray.length;

            int[] contigGapArray = contigGapsArray[chIndex];


            // offset is used to align with ch numbers
            y = horiScaleTextY + SCALE_TEXT_LINE_GAP + INTER_CHROMOSOME_GAP * (chIndex + 1)
                - this.getFontMetrics(textFont).getHeight();

            for (int contigIndex = 0; contigIndex < contigArray.length; contigIndex++) {

                Contig contig = contigArray[contigIndex];

                contigSize      = MathUtil.Sum(contig.cloneSizes);
                totalContigSize += contigSize;
                estSize         += contigSize;
                contigPixWidth = (int) ((double) contigSize / (MAX_CH_SIZE_MB * 1000)
                                        * MAX_CH_PIX_SIZE);

                g2.setColor(Color.black);
                g2.drawRect(x, y, contigPixWidth, CONTIG_HEIGHT);


                // draw probes and linking clones with colors
                List<Clone> contigClones = contig.cloneList;

                totalCosmidCloneCount += contigClones.size();

                List<Integer> cloneSizes = contig.cloneSizes;
                int clone_x              = x;
                int clonePixWidth        = 0;

                for (int cloneIndex = 0; cloneIndex < contigClones.size(); cloneIndex++) {

                    g2.setColor(getProbeColor(contigClones.get(cloneIndex)));

                    int cloneSize = cloneSizes.get(cloneIndex).intValue();

                    clonePixWidth = (int) ((double) cloneSize / (MAX_CH_SIZE_MB * 1000)
                                           * MAX_CH_PIX_SIZE);

                    g2.drawRect(clone_x, y, clonePixWidth, CONTIG_HEIGHT);

                    clone_x += clonePixWidth;
                }

                g2.setColor(Color.black);
                g2.drawLine(x + contigPixWidth, y - 5, x + contigPixWidth, y + CONTIG_HEIGHT + 5);    // acting as a contig divider

                if (contigIndex < contigArray.length - 1) {
                    estSize += contigGapArray[contigIndex];
                }

                x = MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP
                    + (int) ((double) estSize / (MAX_CH_SIZE_MB * 1000) * MAX_CH_PIX_SIZE);
            }

            g2.drawString("" + contigArray.length + "/" + probeOrders[chIndex].length,
                          x + contigPixWidth + 5, y);
        }

        System.out.println("Total contig count: " + totalContigCount);
        System.out.println("Total cosmid clone count: " + totalCosmidCloneCount);
        System.out.println("Total contig size: " + totalContigSize);
        System.out.println("");
        System.out.println("Average # of clones per contig : "
                           + (int) (double) totalCosmidCloneCount / totalContigCount);
        System.out.println("Average size of a contig: "
                           + (int) (double) totalContigSize / totalContigCount + " kb");


        // create the map legend
        int legend_box_x = MAP_SCALE_HORI_INDENT + SCALE_TEXT_LINE_GAP + MAX_CH_PIX_SIZE
                           + MAP_LEGEND_HORI_GAP;
        int legend_box_y = MAP_HEADER_VERT_INDENT + this.getFontMetrics(textFont).getHeight()
                           + MAP_HEADER_SCALE_GAP + SCALE_TEXT_LINE_GAP + MAP_LEGEND_VERT_GAP;
        int legend_box_width = 2 * LEGEND_HORI_PAD + LEGEND_FIG_TEXT_GAP
                               + GraphicsUtil.findWordPixelWidth("7",
                                   this.getFontMetrics(textFont)) + LEGEND_WIDTH;
        int legend_box_height = 2 * LEGEND_VERT_PAD + 7 * LEGEND_HEIGHT + 6 * INTER_LEGEND_GAP;

        g2.setColor(Color.black);
        g2.drawRect(legend_box_x, legend_box_y, legend_box_width, legend_box_height);

        int legend_y   = legend_box_y + LEGEND_VERT_PAD;
        int fontHeight = this.getFontMetrics(textFont).getHeight();

        for (int ch_id = 1; ch_id <= 7; ch_id++) {

            g2.setColor(Color.black);
            g2.drawString("" + ch_id, legend_box_x + LEGEND_HORI_PAD, legend_y + fontHeight);
            g2.setColor(getProbeColor(ch_id));
            g2.fillRect(legend_box_x + LEGEND_HORI_PAD + LEGEND_FIG_TEXT_GAP, legend_y,
                        LEGEND_WIDTH, LEGEND_HEIGHT);
            g2.setColor(Color.black);
            g2.drawRect(legend_box_x + LEGEND_HORI_PAD + LEGEND_FIG_TEXT_GAP, legend_y,
                        LEGEND_WIDTH, LEGEND_HEIGHT);

            legend_y += LEGEND_HEIGHT + INTER_LEGEND_GAP;
        }
    }

    @Override
    public int getHeight() {
        return mapHEIGHT;
    }

    @Override
    public int getWidth() {
        return mapWIDTH;
    }

    /**
     * Method description
     *
     * @param clone description
     *
     * @return description
     */
    private Color getProbeColor(Clone clone) {
        return getProbeColor(clone.getLinkageGroupSet().size());
    }

    /**
     * Method description
     *
     * @param linkGrpSize description
     *
     * @return description
     */
    private Color getProbeColor(int linkGrpSize) {

        switch (linkGrpSize) {

            case 1 :
                return Color.white;

            case 2 :
                return Color.red;

            case 3 :
                return Color.gray;

            case 4 :
                return Color.yellow;

            case 5 :
                return Color.green;

            case 6 :
                return Color.blue;

            case 7 :
                return Color.cyan;

            default :
                return Color.white;
        }
    }

    /**
     * Method description
     *
     * @param probeIndex description
     * @param order description
     * @param binaryProfile description
     * @param clones description
     *
     * @return description
     */
    private Clone getLinkingClone(int probeIndex, int[] order, int[][] binaryProfile,
                                  Clone[] clones) {

        List<Clone> linkedClones = new ArrayList<>();

        for (int i = 0; i < binaryProfile[0].length; i++) {

            if ((binaryProfile[order[probeIndex] - 1][i] == 1)
                    && (binaryProfile[order[probeIndex + 1] - 1][i] == 1)) {
                linkedClones.add(clones[i]);
            }
        }

        Clone clone = findSpecificTypeClone(Clone.S_CLONE, linkedClones);

        if (clone == null) clone = findSpecificTypeClone(Clone.R_CLONE, linkedClones);
        if (clone == null) clone = findSpecificTypeClone(Clone.A_CLONE, linkedClones);

        return clone;
    }

    /**
     * Method description
     *
     * @param cloneType description
     * @param cloneList description
     *
     * @return description
     */
    private Clone findSpecificTypeClone(int cloneType, List<Clone> cloneList) {

        Clone result = null;

        for (int cloneIndex = 0; cloneIndex < cloneList.size(); cloneIndex++) {

            Clone clone = cloneList.get(cloneIndex);

            switch (cloneType) {

                case Clone.S_CLONE :
                    if (clone.isSClone()) result = clone;

                    break;

                case Clone.R_CLONE :
                    if (clone.isRClone()) result = clone;

                    break;

                case Clone.A_CLONE :
                    if (clone.isAClone()) result = clone;
            }

            if (clone != null) break;
        }

        return result;
    }

    /**
     * Class description
     *
     * @version Enter version here..., 12/11/23
     * @author Susanta Tewari
     */
    private class Contig {

        List<Clone> cloneList    = null;
        List<Integer> cloneSizes = null;

        Contig(List<Clone> cloneList, List<Integer> cloneSizes) {

            this.cloneList  = cloneList;
            this.cloneSizes = cloneSizes;
        }
    }
}
